import java.net.*;
import java.util.Date;
import java.io.*;

public class Controller {

	/* Input arguments:
	 * 0 - Port that the controller listens to
	 * 1 - Port of the other controller
	 * 2 - IP Address of the other controller
	 * 3 - Number of clients to support 
	 * 4 - Type of Controller (0: Normal | 1: Standby)	 
	 */
	public static void main(String[] args) {
			
		int otherPort = Integer.parseInt(args[1]);
		InetAddress address = null; //IP @ of other controller
		
		try {
			address = InetAddress.getByName(args[2]);
		} catch (UnknownHostException e2) {
			e2.printStackTrace();			
		} 
		
		int clients = Integer.parseInt(args[3]);
		int type = Integer.parseInt(args[4]);	
		
		int aliveClients = clients;
		Socket clientSocks [] = new Socket[clients];
		int[] ports = new int[clients];
		String[] addresses = new String[clients];
		int state[] = new int[clients];
		long recoveryTime[] = new long[clients];
				
		for(int i = 0; i < clients; i++){
			clientSocks[i] = null;
			addresses[i] = null;
			state[i]=0;
			ports[i]=-1;
			recoveryTime[i]=-1;
		}
		
		ServerSocket controller;	
		
		BufferedReader in;
		PrintWriter out;
		String str = null;
		boolean otherControllerExists = true;
		
		/*try{
			Socket s = new Socket(address, otherPort); //try to connect to the other controller	
			s.close();
			in = new BufferedReader(new InputStreamReader(s.getInputStream())); //Read from controller
			out = new PrintWriter(s.getOutputStream(),true); //Send to controller
			out.println("hello");
			
			String l = in.readLine();
			if(l.equals("ACK")){
				type = 1;
				System.out.println("Standby Controller"); 
			}
		}
		catch (Exception e) {
			System.out.println("Basic Controller"); //if connection is not made this means that the controller must be the basic
			otherControllerExists = false;
		}	
		*/
		if(type == 1) aliveClients = 0;
		
		try {
			// 1) Initialize controller data
			controller = new ServerSocket(Integer.parseInt(args[0])); //Initialize the controller with the port
			System.out.println("Controller started");
			
			// 2) Waits for connection of all clients and store data 
			//NORMAL CONTROLLER
			if(type == 0){
				
				for(int i = 0; i < clients; i++){
					clientSocks[i] = controller.accept();
					System.out.println("Connection " + (i) + " accepted: " + clientSocks[i]);
					
					in = new BufferedReader(new InputStreamReader(clientSocks[i].getInputStream()));
					out = new PrintWriter(clientSocks[i].getOutputStream(),true);
					
					//get & store port of client (msg = {'port', localport})
					str = in.readLine();
					String[] line = str.split(" ");
					ports[i] = Integer.parseInt(line[1]);
					
					//get & store address in quad String form
					String add = clientSocks[i].getInetAddress().toString();
					addresses[i] = add.substring(1); 
					
					System.out.println(ports[i] + " " + addresses[i]);
					
					//send ID to client
					out.println(Integer.toString(i));
				}
				
				// 3) When done send data to the other clients
				for(int i = 0; i < clients; i++){				
					out = new PrintWriter((clientSocks[i].getOutputStream()),true);
					
					for(int j = 0; j < clients; j++){
						if(j != i){
							System.out.println("Sending info to client "+ j);
							out.println(addresses[j] + " " + ports[j] + " " + j);	//send ports and addresses to the other clients
						}
					}	
					
					out.println("done");
					out.close();
				}
			
			}
			
			Socket connection = null;
			int recoveredNode = -1;
			boolean patch = false;
			int iterations = 5;
			
			// 4) Waits and listens for updates
			while(true){
				//if(type == 0){
					iterations--;
					if(iterations == 0){ //sends updates to other controller
						try{
							Socket otherC = new Socket(address, otherPort);
							in = new BufferedReader(new InputStreamReader(otherC.getInputStream())); //Read from controller
							out = new PrintWriter(otherC.getOutputStream(),true); //Send to controller
							out.println("replica 0");
							
							String l = in.readLine();
							if(l.equals("ACK")){
								for(int i=0; i<clients; i++){
									out.println(i + " " + state[i] + " " + recoveryTime[i]);
								}						
							}
							System.out.println("Replica sent successfully");
							iterations = 5;
							otherControllerExists = true;
							type = 0;
						}
						catch (ConnectException e) {
							type = 0;
							otherControllerExists = false;
							iterations = 5;
						}					
					}
				//}
				
				System.out.println("waiting... " + aliveClients);
				
				if(type == 0) controller.setSoTimeout(10000); //only in normal controller
				try{
					connection = controller.accept(); //Listen for any connection
				}
				catch (SocketTimeoutException e) {
					if(aliveClients > 1 && !patch){
						for(int i = 0; i< clients; i++){
							if(ports[i] != -1){
								InetAddress addr = InetAddress.getByName(addresses[i]); //IP @ of controller								
								try{
									connection = new Socket(addr, ports[i]);
								}
								catch (ConnectException e1) {
									ports[i] = -1;
									addresses[i] = null;
									clientSocks[i] = null;
									aliveClients--;
									continue;
								}
								out = new PrintWriter(connection.getOutputStream(),true);
								in = new BufferedReader(new InputStreamReader(connection.getInputStream())); 
								out.println("force 0");
								patch = true;
								break;
							}
						}
						if(patch) continue;
					}
				}
				
				in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
				
				while(true){
					try{
						str = in.readLine();
						break;
					}
					catch (SocketException e) {
						
					}
				}
				
				System.out.println(str);
				String [] line = str.split(" ");
				
				/*if(str.equals("hello")){ //hello message from other controller
					out = new PrintWriter(connection.getOutputStream(),true);
					out.println("ACK");
				}
				else*/ if(line[0].equals("FLOODDONE")){
					int rnode = Integer.parseInt(line[1]);
					int node = Integer.parseInt(line[2]);
					
					if(type == 1 && ports[node] == -1){						
						ports[node] = Integer.parseInt(line[3]);						
						String add = connection.getInetAddress().toString();
						addresses[node] = add.substring(1); 
						clientSocks[node] = connection;
						aliveClients++;
					}
					
					if(recoveryTime[rnode] != -1){
						long finish = new Date().getTime();
						recoveryTime[rnode] = finish - recoveryTime[rnode];
						System.out.println("Recovery Time " + ((double)recoveryTime[rnode]/1000) + "s");
					}
				}
				
				else if(line[0].equals("ALIVE")){
					int node = Integer.parseInt(line[1]);
					if(type == 1 && ports[node] == -1){						
						ports[node] = Integer.parseInt(line[2]);						
						String add = connection.getInetAddress().toString();
						addresses[node] = add.substring(1); 
						clientSocks[node] = connection;
						aliveClients++;
					}
					
					out = new PrintWriter(connection.getOutputStream(),true);
					if(aliveClients > 1){ //if more than 1 active client means that the client got into a fault state
						out.println("receive");
						for(int j = 0; j < clients; j++){
							if(ports[j] != -1){
								System.out.println("Sending info of "+ j);
								out.println(addresses[j] + " " + ports[j] + " " + j);	//send ports and addresses to the other clients
							}
						}	
						out.println("done");
					}
					else{
						out.println("ACK");
						continue;
					}
				}	
				
				else if(line[0].equals("replica")){ 
					out = new PrintWriter(connection.getOutputStream(),true);
					out.println("ACK");
					for(int i = 0; i < clients; i++){
						String l = in.readLine();
						String [] sl = l.split(" ");
						state[i] = Integer.parseInt(sl[1]);
						recoveryTime[i] = Long.parseLong(sl[2]);
					}
					System.out.println("Replica received successfully");
				}
				
				else if(line[0].equals("crashed")){ //if node crashed update tables
					
					int node = Integer.parseInt(line[2]);
					if(type == 1 && ports[node] == -1){						
						ports[node] = Integer.parseInt(line[3]);						
						String add = connection.getInetAddress().toString();
						addresses[node] = add.substring(1); 
						clientSocks[node] = connection;
						aliveClients++;
					}
					
					int crashednode = Integer.parseInt(line[1]);
					recoveryTime[crashednode] = new Date().getTime(); //keep time of crasher node
					
					System.out.println("Node " + crashednode + " crashed");
					clientSocks[crashednode] = null;
					addresses[crashednode] = null;
					ports[crashednode] = -1;		
					out = new PrintWriter(connection.getOutputStream(),true);
					out.println("ACK");
					aliveClients--;
				}
				
				else if(line[0].equals("recover")){ //node is trying to recover
					
					if(type == 1){
						System.out.println("CHANGE");
						out = new PrintWriter(connection.getOutputStream(),true);
						out.println("change");
						continue;
					}
					
					int node = Integer.parseInt(line[1]);
					System.out.println("Recovering node " + node);				
					
					//store recovering node's data
					clientSocks[node] = connection;
					ports[node] = Integer.parseInt(line[2]);
					String add = clientSocks[node].getInetAddress().toString();
					addresses[node] = add.substring(1); 
					
					out = new PrintWriter(connection.getOutputStream(),true);
					out.println("recover " + state[node]);
					str = in.readLine();
					
					if(str.equals("ACK")){
						System.out.println("State sent succefully");
					}
					
					for(int j = 0; j < clients; j++){
						if(j != node && ports[j] != -1){
							System.out.println("Sending info to client "+ j);
							out.println(addresses[j] + " " + ports[j] + " " + j);	//send ports and addresses to the other clients
						}
					}	
					
					out.println("done");
					recoveredNode = node;
					
					if(aliveClients == 1){ //case where one node is left in the system and tries to get the recovered node
						connection = controller.accept(); //Listen for any connection
						//in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
						out = new PrintWriter(connection.getOutputStream(),true);
						out.println("recover " + addresses[node] + " " + ports[node] + " " + node);
						out.close();
						recoveredNode = -1;
					}		
					
					aliveClients++;
					
				} //Receiving update message
				else if(line[0].equals("update")){
					System.out.println("Update received");
					int node = Integer.parseInt(line[1]);
				
					state[node] = Integer.parseInt(line[2]); //get update info
					
					if(type == 1 && ports[node] == -1){						
						ports[node] = Integer.parseInt(line[3]);						
						String add = connection.getInetAddress().toString();
						addresses[node] = add.substring(1); 
						clientSocks[node] = connection;
						aliveClients++;
					}
					
					//Save states to file locally
					File file = new File("state.txt"); //if file exists means that the node is recovering
					BufferedWriter output = new BufferedWriter(new FileWriter(file)); //write to file the node id
					for(int i=0; i<clients; i++){
						output.write("" + i + " " + state[i] + "\n");
					}					
					
					output.close();
					
					System.out.println("Data of node " + node + " was updated. Current state:" + state[node]);
					out = new PrintWriter(connection.getOutputStream(),true);
					patch = false;
					if(recoveredNode == -1)
						out.println("ACK");
					else{
						out.println("recover " + addresses[recoveredNode] + " " + ports[recoveredNode] + " " + recoveredNode);
						recoveredNode = -1; //reset recovered node id
					}
					
					connection.close();
					in.close();
					out.close();					
				}				
			}
		}
		catch (Exception e) {
			e.printStackTrace();
		}		
	}
}
